/**
 * Created by henian.xu on 2020/1/14.
 * vue-navigation 1.1.4
 * see https://github.com/zack24q/vue-navigation
 */
import {
  getUniqueId,
  isDef,
  hasOwn,
  isObjEqual,
  Device,
  isString,
  arraySomeAsync,
} from 'utils/index';
import {
  bus,
  setKeyName,
  setRouter,
  getRouter,
  setEntranceKeyName,
} from './runtime';
import NavComponents from './NavComponents';
import { record, reset } from './navigator';
import { getRoutes, setRoutes } from './routes';

function quit() {
  const promise = new Promise((resolve, reject) => {
    if (!bus._events.quit || !bus._events.quit.length) {
      resolve();
    } else {
      bus.$emit('quit', { resolve, reject });
    }
  });
  promise
    .then(() => {
      if (Device.weixin && !Device.weixinMiniProgram) {
        bus.$wxSDK.closeWindow();
      } else {
        getRouter().back();
      }
    })
    .catch(() => {
      getRouter().forward();
    });
}

export default (vue, { router, keyName = 'VNK', entranceKeyName = 'ENT' }) => {
  if (!router) console.error('vue-navigation need options: router');
  setKeyName(keyName);
  setEntranceKeyName(entranceKeyName);
  setRouter(router);

  // hack vue-router replace for replaceFlag
  const oldRouteReplace = router.replace.bind(router);
  let replaceFlag = false;
  // eslint-disable-next-line no-param-reassign
  router.replace = (location, onComplete, onAbort) => {
    replaceFlag = true;
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        return oldRouteReplace(location, resolve, (err) => {
          if (isDef(err) && err.type !== 1 && err.type !== 3) {
            reject(err);
          } else {
            setTimeout(() => {
              // 为了解决在 beforeEach,beforeResolve 中调用
              // router.replace 方法时 router.afterEach 没有触的问题
              replaceFlag = false;
            }, 500);
            resolve();
          }
        });
      });
    }
    return oldRouteReplace(location, onComplete, onAbort);
  };
  // hack vue-router push 为了解决 在路径上加 keyName 参数时 Promise 为 reject
  const oldRoutePush = router.push.bind(router);
  // eslint-disable-next-line no-param-reassign
  router.push = (location, onComplete, onAbort) => {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        return oldRoutePush(location, resolve, (err) => {
          if (isDef(err) && err.type !== 1 && err.type !== 3) {
            reject(err);
          } else {
            resolve();
          }
        });
      });
    }
    return oldRoutePush(location, onComplete, onAbort);
  };

  // init router`s keyName
  router.beforeEach((to, from, next) => {
    const key = to.query[keyName];
    const rowLocation = {
      name: to.name,
      params: to.params,
      query: to.query,
    };
    let location;
    if (!key) {
      const query = { ...to.query };
      // 转到相同的路线将设置相同的键
      const isPathEqual = isObjEqual(
        { ...to.query, [keyName]: null },
        { ...from.query, [keyName]: null },
      );
      if (from.query[keyName] && to.path === from.path && isPathEqual) {
        query[keyName] = from.query[keyName];
      } else {
        query[keyName] = getUniqueId();
      }
      location = {
        ...rowLocation,
        query,
        replace: replaceFlag || !from.query[keyName],
      };
    }
    if (Device.weixin && !Device.weixinMiniProgram) {
      const routes = getRoutes();
      if (!routes.length && !hasOwn(rowLocation.query, entranceKeyName)) {
        location = location || rowLocation;
        location.query[entranceKeyName] = null;
      }
    }
    next(location);
  });

  // record router change
  router.afterEach((to, from) => {
    const routesLength = getRoutes().length;
    const justEntered = hasOwn(to.query, entranceKeyName) && !routesLength;
    const shouldQuit = hasOwn(to.query, entranceKeyName) && routesLength;
    // console.log(to, from, routesLength);
    record(to, from, replaceFlag);
    if (Device.weixin && !Device.weixinMiniProgram) {
      if (justEntered) {
        const query = { ...to.query };
        delete query[entranceKeyName];
        router.push({
          ...to,
          query,
        });
      } else if (shouldQuit) {
        quit();
      }
    }
    replaceFlag = false;
  });

  vue.component(NavComponents.name, NavComponents);

  async function routerBack(n) {
    return new Promise((resolve, reject) => {
      let timeId = null;
      const cancel = router.beforeEach((to, from, next) => {
        clearTimeout(timeId);
        cancel();
        next();
        resolve(to);
      });
      router.go(n);
      timeId = setTimeout(() => {
        cancel();
        reject();
      }, 200);
    });
  }

  const navigation = {
    on: (event, callback) => {
      bus.$on(event, callback);
    },
    once: (event, callback) => {
      bus.$once(event, callback);
    },
    off: (event, callback) => {
      bus.$off(event, callback);
    },
    getRoutes: () => getRoutes().slice(),
    cleanRoutes: () => reset(),
    reLaunch: async (location) => {
      const routesLength = getRoutes().length;
      let cb = null;
      const promise = new Promise((resolve) => {
        cb = resolve;
      });
      const cancelAfterEach = router.afterEach((to) => {
        cancelAfterEach();
        cb(to);
      });

      if (routesLength <= 1 /* && Device.weixin */) {
        router.replace(location);
        return promise;
      }
      const cancelBeforeEach = router.beforeEach((to, from, next) => {
        cancelBeforeEach();
        next();
        if (location) {
          if (isString(location)) location = { path: location };
          location.replace = true;
          if (location.path === router.currentRoute.fullPath) return;
          /* if (Device.weixin) */ setRoutes([]);
          router.replace(location);
        }
      });

      await arraySomeAsync([...Array(routesLength).keys()], async (index) => {
        try {
          /* if (Device.weixin) */ setRoutes([]);
          await routerBack(index - (routesLength - 1));
          return true;
        } catch (e) {
          return false;
        }
      });
      return promise;
    },
  };

  Object.defineProperty(vue, 'navigation', {
    get() {
      return navigation;
    },
  });
  Object.defineProperty(vue.prototype, '$navigation', {
    get() {
      return navigation;
    },
  });
};
